package org.snlab.runtime;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;

import org.snlab.interfaces.DIBInf;
import org.snlab.runtime.Action.Mode;

public class DIB implements DIBInf {
    private String deviceName;
    private BDDEngine bddEngine = new BDDEngine(32);
    private TreeSet<Rule> rules = new TreeSet<>(
            (x, y) -> x.getPrefix() == y.getPrefix() ? x.getHit() - y.getHit() : y.getPrefix() - x.getPrefix());
    private Map<String, Integer> portToPred = new HashMap<>();

    private BiMap<Action, Integer> actionToPred = HashBiMap.create();

    public DIB() {

    }

    public DIB(String deviceName) {
        this.deviceName = deviceName;
    }

    public void loadFib(String fn) {
        File file = new File(fn);
        Scanner in = null;
        try {
            in = new Scanner(file);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        while (in.hasNextLine()) {
            String[] tokens = in.nextLine().split(" ");
            long match = Long.parseLong(tokens[1]);
            int prefix = Integer.parseInt(tokens[2]);
            String port = tokens[3].split("\\.")[0];

            int hit = bddEngine.encodeIP(match, prefix);
            Rule rule = new Rule(hit, prefix, port);
            rule.setMatchBdd(hit);
            Set<String> ports = new HashSet<>(Arrays.asList(port.split(",")));
            Action action = new Action(ports, Mode.ALL);
            rule.setAction(action);
            rules.add(rule);
        }
    }

    public BDDEngine getBDDEngine() {
        return this.bddEngine;
    }

    @Override
    public String getDeviceName() {
        return this.deviceName;
    }

    @Override
    public void computeEC() {
        int fwd = 0;
        for (Rule rule : rules) {
            if (!portToPred.containsKey(rule.getPort())) {
                portToPred.put(rule.getPort(), 0);
            }
            int hit = bddEngine.and(rule.getHit(), bddEngine.not(fwd));
            int p = bddEngine.or(portToPred.get(rule.getPort()), hit);
            portToPred.put(rule.getPort(), p);
            fwd = bddEngine.or(fwd, rule.getHit());
            rule.setHit(hit);
        }
    }

    public void computeECx() {
        int fwd = 0;
        for (Rule rule : rules) {
            if (!actionToPred.containsKey(rule.getAction())) {
                actionToPred.put(rule.getAction(), 0);
            }
            int hit = bddEngine.and(rule.getHit(), bddEngine.not(fwd));
            int p = bddEngine.or(actionToPred.get(rule.getAction()), hit);
            actionToPred.put(rule.getAction(), p);
            fwd = bddEngine.or(fwd, rule.getHit());
            rule.setHit(hit);
        }
        System.out.println(this.actionToPred.size());
        updatePortToPred();
    }

    private void updatePortToPred() {
        portToPred.clear();
        for (Map.Entry<Action, Integer> entry : actionToPred.entrySet()) {
            for (String port : entry.getKey().getPorts()) {
                portToPred.computeIfAbsent(port, k -> 0);
                portToPred.put(port, bddEngine.or(portToPred.get(port), entry.getValue()));
            }
        }
        System.out.println(this.portToPred.size());
    }

    public int deleteRule(Rule rule) {
        int changed = 0;
        int rh = rule.getHit();
        for (Rule r : rules.stream().filter(r -> r.getPrefix() < rule.getPrefix()).collect(Collectors.toList())) {

            int intersection = bddEngine.and(r.getMatchBdd(), rh);
            if (intersection != 0 && !rule.getAction().equals(r.getAction())) {
                r.setHit(bddEngine.or(r.getHit(), intersection));
                rh = bddEngine.substract(rh, intersection);
                changed = bddEngine.or(changed, intersection);
                actionToPred.put(r.getAction(), bddEngine.or(actionToPred.get(r.getAction()), intersection));
                for (String port : r.getAction().getPorts()) {
                    portToPred.computeIfAbsent(port, k -> 0);
                    portToPred.put(port, bddEngine.or(portToPred.get(port), intersection));
                }
            }

        }
        if (rh != 0) {
            changed = bddEngine.or(changed, rh);
            if (actionToPred.get(rule.getAction()) == rh) {
                actionToPred.remove(rule.getAction());
            } else {
                actionToPred.put(rule.getAction(), bddEngine.substract(actionToPred.get(rule.getAction()), rh));
            }
        }
        rules.remove(rule);
        return changed;
    }

    public Map<String, Integer> getPortToPred() {
        return portToPred;
    }

    public Map<String, Integer> getModel() {
        return portToPred;
    }

    public Map<Action, Integer> getModelx() {
        return actionToPred;
    }

    @Override
    public void addRule() {
        // TODO Auto-generated method stub

    }

    @Override
    public void dump() {
        // TODO Auto-generated method stub

    }

    /**
     * @param deviceName the deviceName to set
     */
    public void setDeviceName(String deviceName) {
        this.deviceName = deviceName;
    }

    public TreeSet<Rule> getRules() {
        return rules;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            DIB dib = new DIB("deviceName");
            dib.loadFib("../envs/i2/fib/atlaap");
            long s = System.nanoTime();
            dib.computeECx();
            System.out.println(System.nanoTime() - s);
            s = System.nanoTime();
            int c = dib.deleteRule(dib.getRules().first());
            System.out.println(c);
            System.out.println(System.nanoTime() - s);
        }
    }

}
